﻿#region Using Statements
using System;
using System.Collections.Generic;
using GameBase.Screens;
using Microsoft.Xna.Framework;
using StarFox.Graphics;
#endregion

namespace StarFox.ScreenComponents
{
    /// <summary>
    /// Implementation of an asteroid field.
    /// <para>Definition of Update() method.</para>
    /// </summary>
    partial class AsteroidField
    {
        #region AsteroidBoundingSphere

        private BoundingSphere AsteroidBoundingSphere(IAsteroid asteroid)
        {
            BoundingSphere sphere = new BoundingSphere();

            if (asteroid is Asteroid)
            {
                sphere = asteroidBSphere[asteroid.Index].Transform(asteroid.World);
            }
            else if (asteroid is PowerUp)
            {
                sphere = powerUpBSphere[asteroid.Index].Transform(asteroid.World);
            }

            return sphere;
        }

        #endregion

        #region UpdateSectors

        /// <summary>
        /// In case the camera changed its sector position, generate new sectors.
        /// </summary>
        private void UpdateSectors()
        {
            // save previous camera sector number
            LastCamSctPos_X = CurrentCamSctPos_X;
            LastCamSctPos_Z = CurrentCamSctPos_Z;

            // obtain current camera sector number
            CurrentCamSctPos_X = (int)Math.Round(ship.Position.X / SECTOR_SIZE);
            CurrentCamSctPos_Z = (int)Math.Round(-ship.Position.Z / SECTOR_SIZE);

            // changed sector?
            if (CurrentCamSctPos_X != LastCamSctPos_X || CurrentCamSctPos_Z != LastCamSctPos_Z)
            {
                // how many sectors?
                int movedSct_X = CurrentCamSctPos_X - LastCamSctPos_X;
                int movedSct_Z = CurrentCamSctPos_Z - LastCamSctPos_Z;

                #region Asteroids, Small Asteroids

                List<IAsteroid>[,] auxSectorAsteroids = new List<IAsteroid>[NUM_SECTORS_X, NUM_SECTORS_Z];
                List<SmallAsteroid>[,] auxSectorSmallAsteroids = new List<SmallAsteroid>[NUM_SECTORS_X, NUM_SECTORS_Z];

                for (int iz = NUM_SECTORS_Z - 1; iz >= 0; iz--)
                {
                    for (int ix = 0; ix <= NUM_SECTORS_X - 1; ix++)
                    {
                        auxSectorAsteroids[ix, iz] = new List<IAsteroid>();
                        auxSectorSmallAsteroids[ix, iz] = new List<SmallAsteroid>();
                    }
                }

                for (int iz = NUM_SECTORS_Z - 1; iz >= 0; iz--)
                {
                    for (int ix = 0; ix <= NUM_SECTORS_X - 1; ix++)
                    {
                        if (ix >= -movedSct_X && ix <= NUM_SECTORS_X - 1 - movedSct_X &&
                            iz >= -movedSct_Z && iz <= NUM_SECTORS_Z - 1 - movedSct_Z)
                        {
                            // copy sector
                            auxSectorAsteroids[ix, iz] = sectorAsteroids[ix + movedSct_X, iz + movedSct_Z];
                            auxSectorSmallAsteroids[ix, iz] = sectorSmallAsteroids[ix + movedSct_X, iz + movedSct_Z];
                        }
                        else
                        {
                            // generate sector
                            GenerateAsteroidSector(auxSectorAsteroids[ix, iz],
                                                   ix - MIDDLE_SECTOR_X + CurrentCamSctPos_X,
                                                   iz - MIDDLE_SECTOR_Z + CurrentCamSctPos_Z);

                            GenerateSmallAsteroidSector(auxSectorSmallAsteroids[ix, iz],
                                                        ix - MIDDLE_SECTOR_X + CurrentCamSctPos_X,
                                                        iz - MIDDLE_SECTOR_Z + CurrentCamSctPos_Z);
                        }
                    }
                }

                sectorAsteroids = auxSectorAsteroids;
                sectorSmallAsteroids = auxSectorSmallAsteroids;

                #endregion
            }

            UpdateSectorVisibility();
        }

        /// <summary>
        /// Determines whether sectors are visible or not.
        /// </summary>
        private void UpdateSectorVisibility()
        {
            for (int z = MAX_SECTOR_Z; z >= MIN_SECTOR_Z; z--)
            {
                for (int x = MIN_SECTOR_X; x <= MAX_SECTOR_X; x++)
                {
                    int ix = x + MIDDLE_SECTOR_X;
                    int iz = z + MIDDLE_SECTOR_Z;

                    bool isVisible = StarFoxHelper.AngleBetweenVectors(Camera.Forward, sectorDirection[ix, iz]) < VIEWABLE_FOV;

                    // always show inner sectors
                    if (Math.Abs(x) + Math.Abs(z) <= 1)
                    {
                        isVisible = true;
                    }

                    sectorIsVisible[ix, iz] = isVisible;
                }
            }
        }

        #endregion

        #region UpdateAsteroids

        private void UpdateAsteroids()
        {
            UpdateAsteroidPositions();
            UpdateAsteroidCollisions();
            UpdateShipCollisions();
        }

        #endregion

        #region UpdateAsteroidPositions

        private void UpdateAsteroidPositions()
        {
            #region Update Positions

            for (int iz = NUM_SECTORS_Z - 1; iz >= 0; iz--)
            {
                for (int ix = 0; ix <= NUM_SECTORS_X - 1; ix++)
                {
                    for (int num = 0; num < sectorAsteroids[ix, iz].Count; num++)
                    {
                        sectorAsteroids[ix, iz][num].Update();
                    }
                }
            }

            #endregion

            #region Update Sectors

            // check whether asteroids changed sector
            for (int iz = NUM_SECTORS_Z - 1; iz >= 0; iz--)
            {
                for (int ix = 0; ix <= NUM_SECTORS_X - 1; ix++)
                {
                    List<IAsteroid> thisSector = sectorAsteroids[ix, iz];

                    for (int num = 0; num < thisSector.Count; num++)
                    {
                        IAsteroid asteroid = thisSector[num];

                        // determine asteroid current sector number
                        int asteroid_X = (int)Math.Round(asteroid.Position.X / SECTOR_SIZE);
                        int asteroid_Z = (int)Math.Round(-asteroid.Position.Z / SECTOR_SIZE);

                        int asteroid_ix = asteroid_X - CurrentCamSctPos_X + MIDDLE_SECTOR_X;
                        asteroid_ix = (int)MathHelper.Clamp(asteroid_ix, 0, NUM_SECTORS_X - 1);
                        int asteroid_iz = asteroid_Z - CurrentCamSctPos_Z + MIDDLE_SECTOR_Z;
                        asteroid_iz = (int)MathHelper.Clamp(asteroid_iz, 0, NUM_SECTORS_Z - 1);

                        // asteroid changed sector
                        if (asteroid_ix != ix || asteroid_iz != iz)
                        {
                            thisSector.RemoveAt(num--);
                            sectorAsteroids[asteroid_ix, asteroid_iz].Add(asteroid);
                        }
                    }
                }
            }

            #endregion
        }

        #endregion

        #region UpdateAsteroidCollisions

        /// <summary>
        /// Checks every sector for collisions between IAsteroid objects.
        /// </summary>
        private void UpdateAsteroidCollisions()
        {
            for (int iz = NUM_SECTORS_Z - 1; iz >= 0; iz--)
            {
                for (int ix = 0; ix <= NUM_SECTORS_X - 1; ix++)
                {
                    for (int i = 0; i < sectorAsteroids[ix, iz].Count; i++)
                    {
                        IAsteroid asteroid = sectorAsteroids[ix, iz][i];

                        for (int aux_iz = iz + 1; aux_iz >= iz - 1; aux_iz--)
                        {
                            for (int aux_ix = ix - 1; aux_ix <= ix + 1; aux_ix++)
                            {
                                if (aux_ix >= 0 && aux_ix <= NUM_SECTORS_X - 1 &&
                                    aux_iz >= 0 && aux_iz <= NUM_SECTORS_Z - 1)
                                {
                                    for (int j = 0; j < sectorAsteroids[aux_ix, aux_iz].Count; j++)
                                    {
                                        IAsteroid otherAsteroid = sectorAsteroids[aux_ix, aux_iz][j];

                                        if (otherAsteroid != asteroid)
                                        {
                                            if (DetectAsteroidCollision(asteroid, otherAsteroid))
                                            {
                                                HandleAsteroidCollision(asteroid, otherAsteroid);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Detects collision between IAsteroid objects.
        /// </summary>
        /// <param name="asteroid">The IAsteroid object.</param>
        /// <param name="otherAsteroid">The IAsteroid object.</param>
        /// <returns>true if collision; false otherwise.</returns>
        private bool DetectAsteroidCollision(IAsteroid asteroid, IAsteroid otherAsteroid)
        {
            BoundingSphere sphere = AsteroidBoundingSphere(asteroid);
            BoundingSphere otherSphere = AsteroidBoundingSphere(otherAsteroid);

            return sphere.Intersects(otherSphere);
        }

        /// <summary>
        /// Handles collision between IAsteroid objects.
        /// </summary>
        /// <param name="asteroid">The IAsteroid object.</param>
        /// <param name="otherAsteroid">The IAsteroid object.</param>
        private void HandleAsteroidCollision(IAsteroid asteroid, IAsteroid otherAsteroid)
        {
            BoundingSphere sphere = AsteroidBoundingSphere(asteroid);
            BoundingSphere otherSphere = AsteroidBoundingSphere(otherAsteroid);

            #region Position Correction

            // approximate the collision point
            float collisionDistance = sphere.Radius + otherSphere.Radius;
            Vector3 middlePoint = sphere.Center * (otherSphere.Radius / collisionDistance) + (otherSphere.Center * (sphere.Radius / collisionDistance));

            // calculate asteroid positions relative to the collision point
            Vector3 normal = sphere.Center - middlePoint;
            normal.Normalize();
            Vector3 otherNormal = otherSphere.Center - middlePoint;
            otherNormal.Normalize();

            // put both asteroids outside the collision
            asteroid.Position = middlePoint + normal * sphere.Radius * 1.01f;
            otherAsteroid.Position = middlePoint + otherNormal * otherSphere.Radius * 1.01f;

            #endregion

            #region Mass

            // mass is proportional to the volume of a sphere
            float mass = 1.333f * MathHelper.Pi * (float)Math.Pow(sphere.Radius, 3);
            float otherMass = 1.333f * MathHelper.Pi * (float)Math.Pow(otherSphere.Radius, 3);

            #endregion

            #region Collision Response

            // elastic collision

            Vector3 movement = asteroid.Movement;
            Vector3 movement_x = Vector3.Dot(normal, movement) * normal;
            Vector3 movement_y = movement - movement_x;

            Vector3 otherMovement = otherAsteroid.Movement;
            Vector3 otherMovement_x = Vector3.Dot(otherNormal, otherMovement) * otherNormal;
            Vector3 otherMovement_y = otherMovement - otherMovement_x;

            asteroid.Movement = movement_x * (mass - otherMass) / (mass + otherMass);
            asteroid.Movement += otherMovement_x * (2 * otherMass) / (mass + otherMass);
            asteroid.Movement += movement_y;

            otherAsteroid.Movement = movement_x * (2 * mass) / (mass + otherMass);
            otherAsteroid.Movement += otherMovement_x * (otherMass - mass) / (mass + otherMass);
            otherAsteroid.Movement += otherMovement_y;

            #endregion
        }

        #endregion

        #region UpdateShipCollisions

        /// <summary>
        /// Checks for collisions between StarShip and IAsteroid objects.
        /// </summary>
        private void UpdateShipCollisions()
        {
            bool isPassBy = false;
            List<Vector3> passByPos = new List<Vector3>();

            for (int iz = 1; iz >= 0; iz--)
            {
                for (int ix = MIDDLE_SECTOR_X - 1; ix <= MIDDLE_SECTOR_X + 1; ix++)
                {
                    for (int i = 0; i < sectorAsteroids[ix, iz].Count; i++)
                    {
                        IAsteroid asteroid = sectorAsteroids[ix, iz][i];
                        BoundingSphere asteroidSphere = AsteroidBoundingSphere(asteroid);

                        if (ship.CenterSphere.Intersects(asteroidSphere))
                        {
                            #region Center

                            // remove the asteroid so the ship hits it only once
                            sectorAsteroids[ix, iz].RemoveAt(i--);
                            if (asteroid is Asteroid)
                                Collision(this, new AsteroidFieldEventArgs(CollisionType.CENTER, null, null));
                            else if (asteroid is PowerUp)
                                Collision(this, new AsteroidFieldEventArgs(null, (asteroid as PowerUp).Type, null));

                            #endregion
                        }
                        else if (ship.LeftSphere.Intersects(asteroidSphere))
                        {
                            #region Left

                            sectorAsteroids[ix, iz].RemoveAt(i--);
                            if (asteroid is Asteroid)
                                Collision(this, new AsteroidFieldEventArgs(CollisionType.LEFT, null, null));
                            else if (asteroid is PowerUp)
                                Collision(this, new AsteroidFieldEventArgs(null, (asteroid as PowerUp).Type, null));

                            #endregion
                        }
                        else if (ship.RightSphere.Intersects(asteroidSphere))
                        {
                            #region Right

                            sectorAsteroids[ix, iz].RemoveAt(i--);
                            if (asteroid is Asteroid)
                                Collision(this, new AsteroidFieldEventArgs(CollisionType.RIGHT, null, null));
                            else if (asteroid is PowerUp)
                                Collision(this, new AsteroidFieldEventArgs(null, (asteroid as PowerUp).Type, null));

                            #endregion
                        }
                        else if (ship.GlobalSphere.Intersects(asteroidSphere))
                        {
                            isPassBy = true;
                            passByPos.Add(asteroid.Position);
                        }
                    }
                }
            }

            if (isPassBy) { PassBy(this, new AsteroidFieldEventArgs(null, null, passByPos)); }
        }

        #endregion

        #region UpdateLights

        private void UpdateLights(GameTime gameTime)
        {
            float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / ASTEROID_LIGHT_TIME.TotalMilliseconds);

            if (asteroidLight_CW)
            {
                asteroidLightAngle -= MathHelper.TwoPi * timeDelta;
                asteroidLightAngle = MathHelper.Clamp(asteroidLightAngle, 0.0f, MathHelper.TwoPi);
                if (asteroidLightAngle == 0.0f) { asteroidLight_CW = !asteroidLight_CW; }
            }
            else
            {
                asteroidLightAngle += MathHelper.TwoPi * timeDelta;
                asteroidLightAngle = MathHelper.Clamp(asteroidLightAngle, 0.0f, MathHelper.TwoPi);
                if (asteroidLightAngle == MathHelper.TwoPi) { asteroidLight_CW = !asteroidLight_CW; }
            }

            asteroidEffect.DirectionalLight0.Direction = new Vector3(-(float)Math.Cos(asteroidLightAngle), -(float)Math.Sin(asteroidLightAngle), 0.0f);
        }

        #endregion

        #region Update

        /// <summary>
        /// Called when the ScreenComponent3D needs to be updated.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public override void Update(GameTime gameTime)
        {
            #region Testing
#if DEBUG
            if (input.IsShowTestInfo(Screen.PlayerIndex)) { showInfo = !showInfo; }
#endif
            #endregion

            UpdateLights(gameTime);
            UpdateSectors();
            UpdateAsteroids();
        }

        #endregion

        #region Events

        /// <summary>
        /// Raised when a collision with the starship has occurred.
        /// </summary>
        public event EventHandler<AsteroidFieldEventArgs> Collision;

        /// <summary>
        /// Raised when an asteroid passed by close to the starship.
        /// </summary>
        public event EventHandler<AsteroidFieldEventArgs> PassBy;

        /// <summary>
        /// Defines what type a collision is.
        /// </summary>
        public enum CollisionType
        {
            /// <summary>
            /// Collision on the left of the starship.
            /// </summary>
            LEFT,
            /// <summary>
            /// Collision on the center of the starship.
            /// </summary>
            CENTER,
            /// <summary>
            /// Collision on the right of the starship.
            /// </summary>
            RIGHT
        };

        /// <summary>
        /// Defines a custom event argument used by the events defined in AsteroidField class.
        /// </summary>
        public class AsteroidFieldEventArgs : EventArgs
        {
            #region Properties

            /// <summary>
            /// Gets whether the starship collided with an asteroid.
            /// </summary>
            public CollisionType? CollisionType { get; private set; }

            /// <summary>
            /// Gets whether the starship collided with a power-up.
            /// </summary>
            public PowerUp.PowerUpType? PowerUpType { get; private set; }

            /// <summary>
            /// Gets the positions of the asteroids that passed by close to the starship.
            /// </summary>
            public List<Vector3> Positions { get; private set; }

            #endregion

            #region Constructor

            /// <summary>
            /// Initializes a new instance of this class.
            /// </summary>
            /// <param name="collisionType">Collision with an asteroid.</param>
            /// <param name="powerUpType">Collision with a power-up.</param>
            /// <param name="positions">Positions of the asteroids that passed by close to the starship.</param>
            public AsteroidFieldEventArgs(CollisionType? collisionType, PowerUp.PowerUpType? powerUpType, List<Vector3> positions)
            {
                CollisionType = collisionType;
                PowerUpType = powerUpType;
                Positions = positions;
            }

            #endregion
        }

        #endregion
    }
}
